﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;

namespace SharpXML
{
    public class XMLElement
    {
        private Dictionary<string,string> _Attributes = new Dictionary<string,string>();
        protected List<XMLElement> _Children;
        private XMLElement _Parent;
        public string Name;
        public int Size;
        private int position;
        protected string _innerXml;
        protected StringBuilder _text;
        public bool hasEndTag = false;

        public XMLElement() { }

        public XMLElement(string xml)
        {
            this.position = 0;
            int endPos = ParseTag(ref xml);
            Size = endPos;

            if (xml.Substring(endPos - 2, 2) == "/>") return;
            hasEndTag = true;
            string endTag = "</" + Name + ">";
            string tag = "<" + Name + ">";
            int endTagPos = xml.IndexOf(endTag);
            int similarTagPos = xml.IndexOf(tag, endPos);
            while (similarTagPos > 0 && similarTagPos < endTagPos)
            {
                endTagPos = xml.IndexOf(endTag, endTagPos + endTag.Length);
                similarTagPos = xml.IndexOf(tag, similarTagPos + tag.Length);
            }
            if (xml.IndexOf(endTag) < 0) throw new XMLException(this);
            Size = endTagPos + endTag.Length;
            _innerXml = xml.Substring(endPos, endTagPos - endPos);
            xml = null;
        }

        public XMLElement(string xml, int position, XMLElement parent = null)
        {
            this.position = position;
            this._Parent = parent;
            int endPos = ParseTag(ref xml);
            Size = endPos;
            
            if (xml.Substring(endPos - 2,2) == "/>") return;
            hasEndTag = true;
            string endTag = "</" + Name + ">";
            string tag = "<" + Name + ">";
            int endTagPos = xml.IndexOf(endTag);
            int similarTagPos = xml.IndexOf(tag,endPos);
            while (similarTagPos > 0 && similarTagPos < endTagPos)
            {
                endTagPos = xml.IndexOf(endTag, endTagPos + endTag.Length);
                similarTagPos = xml.IndexOf(tag, similarTagPos + tag.Length);
            }
            if (xml.IndexOf(endTag) < 0) throw new XMLException(this);
            Size = endTagPos + endTag.Length;
            _innerXml = xml.Substring(endPos, endTagPos - endPos);
            xml = null;            
        }

        public virtual int Length
        {
            get
            {
                int length = 2 + Name.Length;

                foreach (KeyValuePair<String,String> attribute in _Attributes)
                {
                    length += 1 + attribute.Key.Length;
                    if (attribute.Value.Length > 0) length += 3 + attribute.Value.Length;
                }

                if (!hasEndTag)
                {
                    length += 2;
                    return length;
                }

                length += 3 + Name.Length;

                if (_Children != null)
                {
                    foreach (XMLElement child in _Children) length += child.Length;
                    length += _text.Length;
                }
                else length += _innerXml.Length;
                return length;
            }
        }

        /// <summary>
        /// Get just the Text content of the xml Tag
        /// </summary>
        public string Text
        {
            get
            {
                if (_text == null) getChildren(); 
                return _text.ToString();
            }
            set
            {
                _text = new StringBuilder(value);
            }
        }

        public List<XMLElement> Children
        {
            get
            {
                if (_Children == null) getChildren();
                return _Children;
            }
        }

        public XMLElement Parent
        {
            get
            {
                return _Parent;
            }
        }

        public string innerXml
        {
            get
            {
                
                if (_innerXml == null)
                {
                    StringBuilder xml = new StringBuilder();
                    if (_text != null)
                    {
                        xml.Append(_text);
                        _text = null;
                    }

                    if (_Children != null) 
                    {
                        _innerXml = "";
                    
                        int childrenOffset = 0;
                        foreach (XMLElement child in _Children)
                        {
                            //System.Diagnostics.Debug.WriteLine(child.IndexOf() + ", " + childrenOffset + ", " + xml.Length);
                            xml.Insert(child.IndexOf() + childrenOffset, child.ToString());
                            childrenOffset += child.Length;
                        }
                        _Children = null;
                    }
                    _innerXml = xml.ToString();

                    xml = null;
                }
                
                
                return _innerXml;
            }
            set
            {
                _text = null;
                _Children = null;
                _innerXml = value;
            }
        }

        public Dictionary<string, string> Attributes
        {
            get
            {
                return _Attributes;
            }
        }

        public object Attr(string key)
        {
            if (!_Attributes.ContainsKey(key)) throw new XMLException(this);
            if (!string.IsNullOrEmpty(_Attributes[key])) return _Attributes[key];

            return true;
        }

        public XMLElement Attr(string key, string value)
        {
            _Attributes[key] = value;
            return this;
        }

        public XMLElement Append(object obj)
        {
            this._innerXml = string.Join(null,new string[]{innerXml,obj.ToString()});
            return this;
        }

        public XMLElement Prepend(object obj)
        {
            this._innerXml = string.Join(null,new string[]{obj.ToString(),innerXml});
            return this;
        }

        //public XMLElement After(object obj)
        //{
        //    XMLElement currentXmlElmnt;
        //    while ((currentXmlElmnt = Next()) != null) currentXmlElmnt.position += obj.ToString().Length;
        //    _Parent.Text.Insert(this.position + 1, obj.ToString());
        //    return this;
        //}

        //public XMLElement Before(object obj)
        //{
        //    position += obj.ToString().Length;
        //    XMLElement currentXmlElmnt;
        //    while ((currentXmlElmnt = Next()) != null) currentXmlElmnt.position += obj.ToString().Length;
        //    _Parent.Text.Insert(this.position, obj.ToString());
        //    return this;
        //}

        public XMLElement Next()
        {
            if (_Parent.Children.IndexOf(this) + 1 >= _Parent.Children.Count) return null;
            return _Parent.Children[_Parent.Children.IndexOf(this) + 1];
        }

        public XMLElement Prev()
        {
            if (_Parent.Children.IndexOf(this) - 1 < 0) return null;
            return _Parent.Children[_Parent.Children.IndexOf(this) - 1];
        }

        public XMLElement Child(string name)
        {
            return Children.Where(c => c.Name == name).FirstOrDefault();
        }

        private void getChildren()
        {
            if (_innerXml == null) return;
            _Children = new List<XMLElement>();
            int contentIndex = 0;
            int leftCaratPosition = 0;
            int rightCaratPosition = 0;
            _text = new StringBuilder();
            while (contentIndex < _innerXml.Length)
            {
                leftCaratPosition = _innerXml.IndexOf('<', contentIndex);
                rightCaratPosition = _innerXml.IndexOf('>', contentIndex);
                if (leftCaratPosition < 0 || rightCaratPosition < leftCaratPosition)
                {
                    _text.Append(_innerXml.Substring(contentIndex));
                    break;
                }

                if (contentIndex < leftCaratPosition) _text.Append(_innerXml.Substring(contentIndex, leftCaratPosition - contentIndex));
                contentIndex = leftCaratPosition;

                XMLElement Element = new XMLElement(_innerXml.Substring(contentIndex), _text.Length, this);
                _Children.Add(Element);
                contentIndex += Element.Size;
            }
            //innerXml will need to be overwritten so clear it now, free up some memory
            _innerXml = null;
        }

        public int IndexOf() {
            return position;
        }

        /// <summary>
        /// Parses the XML tag, returns the position of the end of the XML opening tag.
        /// </summary>
        /// <param name="xml"></param>
        /// <returns></returns>
        private int ParseTag(ref string xml)
        {
            if (xml[0] != '<') throw new XMLException(this);
            char[] whiteSpace = { ' ', '\u000A', '\u000B', '\u000C', '\u000D', '\u0085', '\u2028', '\u2029', '\u0009' };
            int firstWhiteSpacePosition = xml.IndexOfAny(whiteSpace);
            int firstRightCaretPosition = xml.IndexOf('>');
            StringBuilder attributeName = new StringBuilder();
            StringBuilder attributeValue = new StringBuilder();
            StringBuilder tempName = new StringBuilder();
            char? useQuote = null;
            int i;

            if (firstRightCaretPosition == -1) throw new XMLException(this);

            if (firstRightCaretPosition < firstWhiteSpacePosition)
            {
                Name = xml.Substring(1, firstRightCaretPosition - 1);
                return firstRightCaretPosition + 1;
            }
            
            if (firstRightCaretPosition > -1 && firstWhiteSpacePosition < 0)
            {
                Name = xml.Substring(1, firstRightCaretPosition - 1);
                return firstRightCaretPosition;
            }
            
            i = Name == null ? 1 : Name.Length + 1;
            while (i < xml.Length && xml[i] != '>')
            {
                if (char.IsWhiteSpace(xml[i]))
                {
                    if (Name == null && tempName.Length > 0)
                        Name = tempName.ToString();
                    i++;
                    continue;
                }

                if (xml[i] == '/')
                {
                    if (i + 1 >= xml.Length || xml[i + 1] != '>') throw new XMLException(this);
                    i++;
                    continue;
                }

                if (string.IsNullOrEmpty(Name))
                {
                    tempName.Append(xml[i]);
                    i++;
                    continue;
                }

                attributeName.Clear();
                attributeValue.Clear();
                useQuote = null;
                while (i < xml.Length && !char.IsWhiteSpace(xml[i]) && xml[i] != '=' && !IsEndCharacter(xml[i]))
                {
                    attributeName.Append(xml[i]);
                    i++;
                }
                _Attributes[attributeName.ToString()] = "";

                while (i < xml.Length && char.IsWhiteSpace(xml[i])) i++;

                if (xml[i] != '=')
                {
                    continue;
                }
                i++;

                while (i < xml.Length && char.IsWhiteSpace(xml[i])) i++;

                if (xml[i] == '"' || xml[i] == '\'')
                {
                    useQuote = xml[i];
                    i++;
                }
                
                while (i < xml.Length &&
                        (
                            (useQuote != null && xml[i] != useQuote) ||
                            (useQuote == null && !char.IsWhiteSpace(xml[i]) && !IsEndCharacter(xml[i]))
                        )
                       )
                {
                    attributeValue.Append(xml[i]);
                    i++;
                }

                _Attributes[attributeName.ToString()] = attributeValue.ToString();
                i++;
            }
            attributeName = null;
            attributeValue = null;
            tempName = null;
            return ++i;
        }

        private static bool IsEndCharacter(char characterToTest) {
            char[] endCharacters = { '>', '/' };
            foreach (char endChar in endCharacters) if (endChar == characterToTest) return true;
            return false;
        }

        public override string ToString()
        {
            StringBuilder xml = new StringBuilder();

            xml.Append('<').Append(Name);

            foreach (KeyValuePair<string, string> attribute in _Attributes)
            {
                xml.Append(' ').Append(attribute.Key);
                if (!string.IsNullOrEmpty(attribute.Value)) xml.Append("=\"").Append(attribute.Value).Append('"');
            }

            if (!hasEndTag)
            {
                xml.Append(" />");
                return xml.ToString();
            }

            xml.Append('>');

            xml.Append(innerXml);

            xml.Append("</").Append(Name).Append('>');
            return xml.ToString();
        }
    }
}
